#!/usr/bin/env ruby
# frozen_string_literal: true

# FreeBSD codegen for KRF
# Like all code generators, this file is ugly.

require "yaml"

PLATFORM = `uname -s`.chomp!.downcase!
CONSERVATIVE = (ARGV.shift == "conservative")

abort "Barf: FreeBSD codegen requested but platform is #{PLATFORM}" if PLATFORM != "freebsd"

HEADER = <<~HEADER
  /* WARNING!
   * This file was generated by KRF's codegen.
   * Do not edit it by hand.
   */
HEADER

SYSCALL_SPECS = Dir[File.join(__dir__, "*.yml")]

SYSCALLS = SYSCALL_SPECS.map do |path|
  spec = YAML.safe_load File.read(path)
  [File.basename(path, ".yml"), spec]
end.to_h

SOURCE_DIR = File.expand_path "../../#{PLATFORM}", __dir__

def hai(msg)
  STDERR.puts "[codegen] #{msg}"
end

hai "output directory: #{SOURCE_DIR}"

gen_files = {
  krf_x: File.open(File.join(SOURCE_DIR, "krf.gen.x"), "w"),
  syscalls_h: File.open(File.join(SOURCE_DIR, "syscalls.gen.h"), "w"),
  syscalls_x: File.open(File.join(SOURCE_DIR, "syscalls.gen.x"), "w"),
  internal_h: File.open(File.join(SOURCE_DIR, "syscalls", "internal.gen.h"), "w"),
}

gen_files.each_value { |file| file.puts HEADER }

SYSCALLS.each do |call, spec|
  # Each syscall requires code generation in 5 files:
  # 1. krf.gen.x, to tell krf that we're interested in faulting it
  # 2. syscalls.gen.h, to prototype the initial wrapper
  # 3. syscalls.gen.x, to set up the initial wrapper
  # 4. syscalls/internal.gen.h, to prototype the internal wrapper
  # 5. syscalls/<syscall>.gen.c, to set up the actual faulty calls

  name = spec["name"] || call
  nr = spec["nr"] || name
  number = "SYS_#{nr}"

  hai "#{call} (nr: #{number})"
  gen_files[:krf_x].puts <<~KRF_X
    krf_faultable_table[#{number}] = (sy_call_t *)&krf_sys_#{call};
  KRF_X

  gen_files[:syscalls_x].puts <<~SYSCALLS_X
    int krf_sys_#{call}(#{spec["proto"]}) {
      __typeof(sys_#{call}) *real_#{name} = (__typeof(sys_#{call}) *)krf_sys_call_table[#{number}];

      if (krf_targeted(KRF_TARGETING_PARMS) && (KRF_RNG_NEXT() % krf_probability) == 0) {
        return krf_sys_internal_#{call}(#{spec["parms"]});
      } else {
        return real_#{name}(#{spec["parms"]});
      }
    }
  SYSCALLS_X

  gen_files[:syscalls_h].puts <<~SYSCALLS_H
    __typeof(sys_#{call}) krf_sys_#{call};
  SYSCALLS_H

  gen_files[:internal_h].puts <<~INTERNAL_H
    __typeof(sys_#{call}) krf_sys_internal_#{call};
  INTERNAL_H

  syscall_c = File.join(SOURCE_DIR, "syscalls", "#{call}.gen.c")
  File.open(syscall_c, "w") do |file|
    file.puts HEADER
    file.puts <<~SETUP
      #include "internal.h"

    SETUP

    fault_table = []
    errors = spec["errors"]
    errors += spec.fetch("unlikely_errors", []) unless CONSERVATIVE
    errors.uniq.each do |fault|
      fault_table << "krf_sys_internal_#{call}_#{fault}"

      file.puts <<~FAULT
        static int krf_sys_internal_#{call}_#{fault}(#{spec["proto"]}) {
          if (krf_log_faults) {
            uprintf("faulting #{call} with #{fault}\\n");
          }

          return #{fault};
        }
      FAULT
    end

    file.puts <<~TRAILER
      static __typeof(sys_#{call})(*fault_table[]) = {
        #{fault_table.join ", "}
      };

      // Fault entrypoint.
      int krf_sys_internal_#{call}(#{spec["proto"]}) {
        return fault_table[KRF_RNG_NEXT() % NFAULTS](#{spec["parms"]});
      }
    TRAILER
  end
end

gen_files.each_value(&:close)
